﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

using ABCat.Core;
using ABCat.Core.Plugins;
using ABCat.Core.Plugins.DataProviders;
using ABCat.Core.Plugins.DataSets;
using ABCat.Core.Plugins.Sites;
using ABCat.Shared;
using ABCat.Shared.Properties;

using HtmlAgilityPack;

namespace ABCat.Plugins.Parsers.Rutracker
{
	[PluginCreator("Rutracker AudioBooks Parser", WorkModeEnum.PerContext), UsedImplicitly]
	public class SiteParserRutrackerAudioBooks : SiteParserBase
	{
		#region + Fields +

		private const int RecordsOnPageCount = 50;

		private DateTime _lastLoadFromInternet;

		#endregion
		#region + Ctor +

		public SiteParserRutrackerAudioBooks(Context context)
			: base(context)
		{
		}

		#endregion
		#region + Logic +

		public override bool CheckForConfig(bool correct, out Config incorrectConfig)
		{
			incorrectConfig = null;
			return true;
		}

		public override IEnumerable<IAudioBookGroup> GetAllRecordGroups(IDbContainer dbContainer)
		{
			var groupKeys = new[] { "2389", "2388", "2387", "1036", "399", "400", "402", "490", "499", "574", "403", "716", "2165", "695" };
			var result = groupKeys.Select(groupKey => GetRecordGroup(groupKey, dbContainer));
			dbContainer.SaveChanges();
			return result;
		}

		private string GetRecordPageUrl([NotNull] string key)
		{
			return @"http://rutracker.org/forum/viewtopic.php?t={0}".F(key);
		}

		protected override void ClearRecordPage(HtmlDocument document)
		{
			base.ClearRecordPage(document);

			var pageHeader = document.GetElementbyId("page_header");
			if (pageHeader != null) pageHeader.ParentNode.RemoveChild(pageHeader);
			var pageFooter = document.GetElementbyId("page_footer");
			if (pageFooter != null) pageFooter.ParentNode.RemoveChild(pageFooter);
			var mainContentWrap = document.GetElementbyId("main_content_wrap");

			if (mainContentWrap != null)
			{
				var topicMain = document.GetElementbyId("topic_main");
				if (topicMain != null)
				{
					var attach = topicMain.GetNodesByClass("fieldset", "attach").FirstOrDefault();
					if (attach != null) attach.ParentNode.RemoveChild(attach);
					foreach (var htmlNode in mainContentWrap.ChildNodes.Where(item => item.Name == "table" && item != topicMain).ToArray())
					{
						htmlNode.ParentNode.RemoveChild(htmlNode);
					}
				}
			}
		}

		[CanBeNull]
		protected override string GetRecordSourcePageString([NotNull] string recordKey, bool cacheFirst, CancellationToken cancellationToken)
		{
			string result = null;

			using (var dbContainer = Context.CreateDbContainer(true))
			{
				var record = dbContainer.AudioBookSet.CreateRecord();
				record.Key = recordKey;
				var pageData = dbContainer.BinaryDataSet.GetByKey(record.GetPageKey());
				if (pageData == null)
				{
					using (var webClient = WebClientPool.GetPoolItem())
					{
						string pageUrl = GetRecordPageUrl(recordKey);
						result = webClient.Target.DownloadString(pageUrl);

						var document = new HtmlDocument();
						document.LoadHtml(result);
						if (cancellationToken.IsCancellationRequested) return null;

						ClearRecordPage(document);
						result = document.DocumentNode.InnerHtml;

						if (cancellationToken.IsCancellationRequested) return null;
						pageData = dbContainer.BinaryDataSet.CreateBinaryData();
						pageData.Key = record.GetPageKey();
						pageData.SetString(result, true);
						dbContainer.BinaryDataSet.AddChangedBinaryData(pageData);
					}
				}
				else result = pageData.GetString();
			}

			return result;
		}

		protected override void DownloadRecord(IDbContainer dbContainer, IAudioBook record, bool cacheFirst, Action<int, int, string> progressCallback, CancellationToken cancellationToken)
		{
			string pageHtml = null;

			if (cacheFirst)
			{
				string pageMetaID = record.GetPageMetaKey();
				var metaPage = dbContainer.BinaryDataSet.GetByKey(pageMetaID);
				if (metaPage != null)
				{
					pageHtml = metaPage.GetString();
				}
			}

			if (pageHtml.IsNullOrEmpty()) pageHtml = DownloadRecordMetaPageFromWeb(record, dbContainer, cancellationToken);

			if (pageHtml.IsNullOrEmpty() || cancellationToken.IsCancellationRequested) return;

			ParseRecord(dbContainer, record, pageHtml);
		}

		protected override void DownloadRecordGroup(IDbContainer dbContainer, IAudioBookGroup audioBookGroup, Action<int, int, string> progressCallback, CancellationToken cancellationToken)
		{
			dbContainer.SaveChanges();

			string url = "http://rutracker.org/forum/viewforum.php?f={0}".F(audioBookGroup.Key);

			int pageIndex = 0;
			int pageCount = 0;

			do
			{
				progressCallback(pageIndex, pageCount, "Загрузка списка '{0}'".F(audioBookGroup.Title));

				string pageHtml;

				using (var webClient = WebClientPool.GetPoolItem())
				{
					string pageUrl = @"{0}&start={1}".F(url, pageIndex * RecordsOnPageCount);
					pageHtml = webClient.Target.DownloadString(pageUrl);
					dbContainer.WaitForParse.Enqueue(pageHtml);
					if (cancellationToken.IsCancellationRequested) return;
				}

				if (pageCount == 0)
				{
					var document = new HtmlDocument();
					document.LoadHtml(pageHtml);
					UpdateRecordGroupInfo(audioBookGroup, document);
					dbContainer.AudioBookGroupSet.AddChangedRecordGroups(audioBookGroup);
					pageCount = audioBookGroup.LastPageCount;
				}

				if (dbContainer.WaitForParse.Count() == 10 || pageIndex == pageCount - 1)
				{
					ParseRecordGroupPages(dbContainer, audioBookGroup, cancellationToken);
				}

				Thread.Sleep(200);

				pageIndex++;
			}
			while (pageIndex < pageCount);

			if (cancellationToken.IsCancellationRequested) return;
			audioBookGroup.LastUpdate = DateTime.Now;
		}

		protected override void ParseRecord(IDbContainer dbContainer, IAudioBook record, string postBodyHtml)
		{
			record.ClearMetaInfo();

			var elementsByRows = ParsePostBodyElementsByRows(postBodyHtml);

			foreach (var element in elementsByRows)
			{
				FillRecordElement(record, element.Key.TrimEnd(':'), element.Value);
			}

			record.LastUpdate = DateTime.Now;
		}

		private static Int32 GetRecordGroupPageCount(HtmlDocument document)
		{
			var pagination = document.GetElementbyId("pagination");
			int lastPageNumber = 1;

			if (pagination != null)
			{
				var pg = pagination.GetNodesByClass("p", null).ToArray();

				if (pg.AnySafe())
				{
					var p = pg.Skip(1).Take(1).First();
					lastPageNumber = p.GetNodesByClass("a", "pg").Select(item =>
					{
						int a = 0;
						Int32.TryParse(item.InnerText, out a);
						return a;
					}).Max();
				}
			}

			return lastPageNumber;
		}

		private static string GetRecordGroupTitle(HtmlDocument document)
		{
			var head = document.DocumentNode.GetNodesByClass("head", null).FirstOrDefault();
			if (head == null) throw new Exception("Cannot find element 'head'");
			var title = head.GetNodesByClass("title", null).FirstOrDefault();
			if (title == null) throw new Exception("Cannot find element 'title'");
			return title.InnerText.Split(new[] { "[стр. 1]" }, StringSplitOptions.RemoveEmptyEntries)[0].Trim();
		}

		private static IEnumerable<HtmlNode> GetTopics(HtmlDocument document)
		{
			var mainContentWrap = document.GetElementbyId("main_content_wrap");
			return mainContentWrap.GetNodesByClass("tr", "hl-tr");
		}

		private static string GetTopicTitle(HtmlNode topicNode)
		{
			string result = topicNode.GetNodesByClass("a", "topictitle tt-text").First().InnerText.Replace("&quot;", "\"");
			result = result.Split('[')[0];
			return result;
		}

		private string DownloadRecordMetaPageFromWeb(IAudioBook record, IDbContainer dbContainer, CancellationToken cancellationToken)
		{
			while ((DateTime.Now - _lastLoadFromInternet).TotalSeconds < 3)
			{
				Thread.Sleep(200);
				if (cancellationToken.IsCancellationRequested) return null;
			}

			string page;
			using (var webClient = WebClientPool.GetPoolItem())
			{
				string pageUrl = GetRecordPageUrl(record.Key);
				page = webClient.Target.DownloadString(pageUrl);
				_lastLoadFromInternet = DateTime.Now;
				if (cancellationToken.IsCancellationRequested) return null;
			}

			var document = new HtmlDocument();
			document.LoadHtml(page);
			if (cancellationToken.IsCancellationRequested) return null;

			ClearRecordPage(document);
			page = document.DocumentNode.InnerHtml;

			var pageKey = record.GetPageKey();

			var binaryData = dbContainer.BinaryDataSet.CreateBinaryData();
			binaryData.Key = pageKey;
			binaryData.SetString(page, true);
			dbContainer.BinaryDataSet.AddChangedBinaryData(binaryData);

			var result = document.DocumentNode.GetNodesByClass("div", "post_body").FirstOrDefault();

			if (result != null)
			{
				var pageMetaBinaryData = dbContainer.BinaryDataSet.CreateBinaryData();
				pageMetaBinaryData.Key = record.GetPageMetaKey();
				pageMetaBinaryData.SetString(result.InnerHtml, false);
				dbContainer.BinaryDataSet.AddChangedBinaryData(pageMetaBinaryData);
			}
			else
			{
				// ToDo: Скорее всего топик был удалён с сайта. Нужна проверка что это именно так - парсинг страницы удалённого топика и пометка записи как удалённой
				return null;
			}

			return result.InnerHtml;
		}

		private void FillRecordElement(IAudioBook record, string key, string value)
		{
			switch (key.ToLower())
			{
				case "автор":
				case "авторы":
				case "фамилия, имя автора":
				case "фамилии авторов":
				case "авторы произведений":
					record.Author = CleanupRecordValue(value, false, 500).ChangeCase(Extensions.CaseTypeEnum.AllWords, true, true);
					break;
				case "фамилия автора":
					record.AuthorSurnameForParse = CleanupRecordValue(value, false, 250).ChangeCase(Extensions.CaseTypeEnum.AllWords, true, true);
					break;
				case "имя автора":
					record.AuthorNameForParse = CleanupRecordValue(value, false, 250).ChangeCase(Extensions.CaseTypeEnum.AllWords, true, true);
					break;
				case "издательство":
					record.Publisher = CleanupRecordValue(value, false, 500).ChangeCase(Extensions.CaseTypeEnum.FirstWord, true, true);
					break;
				case "исполнитель":
				case "исполнители":
				case "запись и обработка":
					record.Reader = CleanupRecordValue(value, false, 500).ChangeCase(Extensions.CaseTypeEnum.AllWords, true, true);
					break;
				case "жанр":
					record.Genre = CleanupRecordValue(value, false, 200).ChangeCase(Extensions.CaseTypeEnum.AfterSplitter, false, true);
					break;
				case "битрейт аудио":
					record.Bitrate = CleanupRecordValue(value, false, 100).ChangeCase(Extensions.CaseTypeEnum.FirstWord, true, true);
					break;
				case "длительность":
				case "прдолжительность":
				case "продолжительность":
				case "общее время звучания":
				case "bремя звучания":
				case "время звучания":
				case "время воспроизведения":
				case "продолжительность аудиокниги":
				case "время":
					record.Lenght = CleanupRecordValue(value, false, 500).ChangeCase(Extensions.CaseTypeEnum.AllWords, true, true);
					var l = record.Lenght.ToTimeSpan();
					break;
				case "описание":
				case "аннотация":
					record.Description = CleanupRecordValue(value, true, 1000);
					break;
				case "доп. информация":
					string secondKey;
					string secondValue;
					if (ParseKeyValue(value, out secondKey, out secondValue))
					{
						FillRecordElement(record, secondKey, secondValue);
					}
					break;
			}
		}

		private IAudioBookGroup GetRecordGroup(string recordGroupKey, IDbContainer dbContainer)
		{
			var result = dbContainer.AudioBookGroupSet.GetRecordGroupByKey(recordGroupKey);
			if (result == null)
			{
				result = dbContainer.AudioBookGroupSet.CreateRecordGroup();
				dbContainer.AudioBookGroupSet.AddRecordGroup(result);
				result.Key = recordGroupKey;
				result.Title = recordGroupKey;
			}
			return result;
		}

		private bool ParseKeyValue(string keyValue, out string key, out string value)
		{
			bool result = false;

			key = null;
			value = null;

			if (!keyValue.IsNullOrEmpty())
			{
				int iofColon = keyValue.IndexOf(':');
				if (iofColon > 0 && iofColon < keyValue.Length - 1)
				{
					key = keyValue.Substring(0, iofColon).ToLower();
					int iofSemiColon = key.LastIndexOf(';');
					if (iofSemiColon > 0)
					{
						key = key.Substring(iofSemiColon + 1, key.Length - iofSemiColon - 1);
					}
					key = key.TrimStart('-', '\n', '.').Trim(' ');

					if (!key.IsNullOrEmpty())
					{
						value = keyValue.Substring(iofColon + 1, keyValue.Length - iofColon - 1);
						result = true;
					}
				}
			}

			return result;
		}

		private Dictionary<string, string> ParsePostBodyElements(string postBodyHtml)
		{
			var result = new Dictionary<string, string>();

			var document = new HtmlDocument();
			document.LoadHtml(postBodyHtml);

			var allNodes = document.DocumentNode.EnumerateAllNodes().ToArray();

			for (int z = 0; z < allNodes.Length - 1; z++)
			{
				var postNode = allNodes[z];

				if (postNode.Name == "span" && postNode.GetAttributeValue("class", "") == "post-b")
				{
					string currentElementName = postNode.InnerText;

					HtmlNode next;
					string text = null;
					bool br = false;
					bool breakFound = false;

					do
					{
						z++;
						next = allNodes[z];
						text += next.InnerText;
						if (z == allNodes.Length - 1) br = true;
						else if (next.Name == "br")
						{
							text += "\r\n";
							breakFound = true;
						}
						else if (next.Name == "span" && next.GetAttributeValue("class", "") == "post-br")
						{
							text += "\r\n";
							breakFound = true;
						}
						else if (z < allNodes.Length - 2 && breakFound && allNodes[z + 1].Name == "span" && allNodes[z + 1].GetAttributeValue("class", "") == "post-b") br = true;
					}
					while (!br);

					text = CleanupRecordValue(text, true, 0);
					if (!string.IsNullOrEmpty(text))
					{
						result[currentElementName] = text;
					}
				}
				//else if (postNode.Name == "br" || (postNode.Name == "span" && postNode.GetAttributeValue("class", "") == "post-br")) currentElementName = null;
				//else if (postNode.Name == "#text" && !string.IsNullOrEmpty(currentElementName))
				//{
				//	string elementText = null;

				//	if (result.TryGetValue(currentElementName, out elementText)) elementText = "{0}\r\n{1}".F(elementText, postNode.InnerText);
				//	else elementText = postNode.InnerText;

				//	elementText = CleanupRecordValue(elementText, true);

				//	if (!string.IsNullOrEmpty(elementText))
				//	{
				//		result[currentElementName] = elementText;
				//	}
				//}
			}

			return result;
		}

		private Dictionary<string, string> ParsePostBodyElementsByRows([NotNull] string postBodyHtml)
		{
			var result = new Dictionary<string, string>();

			var rows = postBodyHtml
				.Replace("</span>", "")
				.Replace("<span>", "")
				.Split(new[] { "<br>", "post-hr" }, StringSplitOptions.RemoveEmptyEntries);

			foreach (var row in rows)
			{
				//int iof = row.IndexOf('>');
				//if (iof < row.Length)
				{
					var doc = new HtmlDocument();
					doc.LoadHtml(row);
					var str = doc.DocumentNode.InnerText;
					string key;
					string value;

					if (ParseKeyValue(str, out key, out value))
					{
						result[key] = value;
					}
				}
			}

			return result;
		}

		private void ParseRecordGroupPage(IDbContainer dbContainer, IAudioBookGroup recordGroup, HtmlDocument document, CancellationToken cancellationToken)
		{
			var topics = new Dictionary<string, HtmlNode>();

			foreach (var htmlNode in GetTopics(document))
			{
				var topicId = htmlNode.GetNodesByClass("td", "topic_id").First().GetAttributeValue("id", "");
				if (dbContainer.RecordsCache.Contains(topicId)) continue;
				dbContainer.RecordsCache.Add(topicId);
				topics.Add(topicId, htmlNode);
			}

			var records = dbContainer.AudioBookSet.GetRecordsByKeys(new HashSet<string>(topics.Keys)).ToDictionary(record => record.Key, record => record);

			foreach (var topic in topics)
			{
				IAudioBook audioBook;
				if (!records.TryGetValue(topic.Key, out audioBook))
				{
					audioBook = dbContainer.AudioBookSet.CreateRecord();
					audioBook.Created = DateTime.Now;
					audioBook.Key = topic.Key;
					audioBook.Genre = Context.GenreDefaultName;
					audioBook.Author = Context.AuthorDefaultName;
					records.Add(topic.Key, audioBook);
					dbContainer.AudioBookSet.AddRecord(audioBook);
				}

				var topicTitle = GetTopicTitle(topic.Value);

				if (cancellationToken.IsCancellationRequested) return;

				audioBook.GroupKey = recordGroup.Key;
				audioBook.Title = topicTitle;
			}
		}

		private void ParseRecordGroupPages(IDbContainer dbContainer, IAudioBookGroup recordGroup, CancellationToken cancellationToken)
		{
			while (dbContainer.WaitForParse.Any())
			{
				string pageHtml = dbContainer.WaitForParse.Dequeue();
				if (cancellationToken.IsCancellationRequested) return;
				var document = new HtmlDocument();
				document.LoadHtml(pageHtml);

				ParseRecordGroupPage(dbContainer, recordGroup, document, cancellationToken);
			}
		}

		private void UpdateRecordGroupInfo(IAudioBookGroup recordGroup, HtmlDocument document)
		{
			recordGroup.Title = GetRecordGroupTitle(document);
			recordGroup.LastPageCount = GetRecordGroupPageCount(document);
		}

		#endregion
	}
}